About this document
Starting point
A project to add some more control to the PS5 Kerbal simulation.
After installing Kerbal on the PS5 and playing it for a while it quickly became clear that the controllers alone are not enough to enjoy the game. The search began to find some way of improving things. Kerbal allows control vi the PS5 controllers AND via keyboard and mouse. This means it should be possible to add some easy toggles/switches/rotary encoders to pass information to the game. The first switch would be the space bar which kerbal uses to trigger stages.
kerbal key bindings
The following is a list of key bindings we can work with.
Arduino - pro micro (5V)
Initial choice for a prototype is the AVR ATmega32u4 8-bit microcontroller which has a USB controller and can therefore be used as both a keyboard and mouse if required.
The Pro Micro is an Arduino-compatible microcontroller board developed under an open hardware license by Sparkfun. Clones of the Pro Micro are often used as a lower-cost alternative to a Teensy 2.0 as a basis for a DIY keyboard controller/converter when a lower number of pins would suffice.
WS2812 LEDs
Because it’s always good to have status LEDs an the WS2812 is one that is both easy to get and has good libraries with fastled and adafruit.
The fastled library looks like a good choice although it doesn’t (yet) support RGBW LEDs for which I have a LED ring. The other LED rings I have are less tightly packed with LEDs. As I have a few WS2812 strips on top of the one RGBW ring the choice went towards the WS2812 to be able to mix the strip and ring. RGBW is better suited to lighting anyway so let’s use it for that later.
Step 1 - First prototype
To get started I went to an example for LEDs to be able to later set a LED with a key/button.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <FastLED.h>
#define NUM_LEDS 22
#define NUM_RING_LEDS 12
#define DATA_PIN 7
CRGB leds[NUM_LEDS];
void setup() {
FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, NUM_LEDS);
leds[12] = CHSV(0, 255, 16);
leds[13] = CHSV(33, 255, 16);
leds[14] = CHSV(65, 255, 16);
leds[15] = CHSV(97, 255, 16);
leds[16] = CHSV(129, 255, 16);
leds[17] = CHSV(161, 255, 16);
leds[18] = CHSV(193, 255, 16);
leds[19] = CHSV(225, 255, 16);
leds[20] = CHSV(255, 255, 16);
leds[21] = CHSV(255, 255, 16);
FastLED.show();
}
void loop() {
for(int dot = 0; dot < NUM_RING_LEDS; dot++) {
leds[dot] = CHSV(64, 255, 16);
FastLED.show();
// clear this led for the next time around the loop
leds[dot] = CRGB::Black;
delay(150);
}
}
The first test looks good.
The above code runs a LED around the ring and sets static colours on the strip. A good start.
Step 2 - Attach a button to trigger a stage
Since this will be starting rockets let’s make it feel that way. Just the space bar and maybe a toggle to arm it.
-
keyboard
-
Button
-
State change detection
We will need to pull down/up the pin for the button so the above resistor is included to show an example 10K Ohm resistor.
The big red button is to trigger a stage and the toggle switch is to arm it.
Bouncy Bouncy
Why all the fuss about bounce? And what is it?
1
2
3
4
5
6
7
8
9
10
11
void my_interrupt_handler()
{
static unsigned long last_interrupt_time = 0;
unsigned long interrupt_time = millis();
// If interrupts come faster than 200ms, assume it's a bounce and ignore
if (interrupt_time - last_interrupt_time > 200)
{
... do your thing
}
last_interrupt_time = interrupt_time;
}
The above short section shows a debounced interrupt that uses millis instead of delays. The important thing here is that if we debounce with delays we stall the whole loop so that if we have a LED animation or something else running it get’s stalled. using millis allows the rest to keep running.
Let’s see if that works.
first iteration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const int buttonPin = 9;
const int ledPin = 13;
int ledState = HIGH;
int buttonState;
int lastButtonState = LOW;
unsigned long lastDebounceTime = 0;
unsigned long debounceDelay = 50;
void setup() {
pinMode(buttonPin, INPUT);
pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, ledState);
}
void loop() {
int reading = digitalRead(buttonPin);
if (reading != lastButtonState) {
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) > debounceDelay) {
if (reading != buttonState) {
buttonState = reading;
if (buttonState == HIGH) {
ledState = !ledState;
}
}
}
digitalWrite(ledPin, ledState);
lastButtonState = reading;
}
The above code sort of works as a debounced latching button. The aim though is to read a debounced button and to read it’s state changes.
-
ONLY when the button is pressed
-
output state ONCE
-
-
When button is released
-
Output state once
-
Second iteration
1
Unresolved directive in README.asciidoc - include::./button-test2/button-test2.ino[]
Step 3 - bring things together to see how the loop runs with buttons
|
Work in Progress |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <FastLED.h>
//fastled stuff
#define NUM_LEDS 22
#define NUM_RING_LEDS 12
#define DATA_PIN 7
#define disarmed_LED 12
#define armed_LED 13
#define SAS_LED 14
#define rot1p_LED 15
#define rot1m_LED 16
#define rot1b_LED 17
CRGB leds[NUM_LEDS];
//button stuff
const int buttonPin = 8;
const int ledPin = LED_BUILTIN;
//start with led OFF
int ledState = LOW;
int buttonState;
//unpressed is assumd
int lastButtonState;
unsigned long lastDebounceTime = 0;
unsigned long debounceDelay = 5;
void setup() {
//LED stuff
FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, NUM_LEDS);
leds[disarmed_LED] = CRGB::Red;
leds[armed_LED] = CRGB::Black;
leds[SAS_LED] = CRGB::Black
leds[rot1p_LED] = CRGB::Black
leds[rot1m_LED] = CRGB::Black;
leds[rot1b_LED] = CRGB::Black;
//spare
leds[18] = CHSV(0, 255, 16);
leds[19] = CHSV(63, 255, 16);
leds[20] = CHSV(127, 255, 16);
leds[21] = CHSV(255, 255, 16);
//END LEDS
FastLED.show();
//button stuff
//default unpressed=HIGH
pinMode(buttonPin, INPUT_PULLUP);
pinMode(ledPin, OUTPUT);
digitalWrite(ledPin, ledState);
int buttonState = digitalRead(buttonPin);
lastButtonState=buttonState;
//The rest
Serial.begin(115200);
Serial.println("setup complete");
}
void loop() {
for(int dot = 0; dot < NUM_RING_LEDS; dot++) {
leds[dot] = CHSV(64, 255, 16);
FastLED.show();
// clear this led for the next time around the loop
leds[dot] = CRGB::Black;
delay(150);
}
}
Step 4 - rotary encoders
Appendix A: Requirements
-
Control Kerbal on PS5
-
Via USB(A) keyboard interface
-
Use big red button with latch for stages
-
-
Must show actions/button presses
-
WS2812 for key status changes red/green/etc
-
-
Add some safety toggles (arm/disarm)
-
A classic toggle with red cover
-
-
Control
-
Stage trigger (space bar)
-
SAS (on/off) (t)
-
gear (up/down) (g)
-
time warp (rotary +/-) (.,)
-
throttle (rotary +/-) (shift,cntrl)
-
motors (on/off) (x,k)
-
View (inside/outside) ???
-
-
This should cover the most required buttons and should be possible without multiplexing.
Appendix B: Interface design thoughts
Since the first button is a big red one with a latch it make sense to also show what state it’s in. Adding a LED ring around it sounds like a good idea. Adding a LED ring around a rotary encoder also sounds like a good idea (optional).
Arm/disarm toggle
- toggle disarmed
-
Arm led LED green(?), latch ring red blink(?)
- toggle armed
-
ARM led red, latch ring green
Triger stage button
- unlatched
-
ring green
- press
-
ring red for 1 sec
- latched
-
Ring orange
Appendix C: 3d printed test stand
The Aim here is to have a stand to mount the buttons and LEDs to while testing. After testing this can be used as a template for drilling. Also this can later be adapted to make a holder for the led ring and potentially the leds that can be mounted under the lid of the box.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
$fn=360;
// mm for slyrs box
topWidth=117;
topDepth=106;
topThick=2;
pillar=4;
height=46;
BigRedButtonD=22;
ToggleD=12;
WS2812D=4;
WS2812RingR=42/2;
numLEDs=12;
//versioning
letter_size = 10;
letter_height = topThick/2;
font = "Liberation Sans";
Version = "V 0.3" ;
module letter(l) {
linear_extrude(height = letter_height) {
text(l, size = letter_size, font = font, halign = "center", valign = "center", $fn = 16);
}
}
//Legs are only needed during prototype phase
translate([0,0,0]) cube([pillar,pillar,height]);
translate([topWidth-pillar,0,0]) cube([pillar,pillar,height]);
translate([topWidth-pillar,topDepth-pillar,0]) cube([pillar,pillar,height]);
translate([0,topDepth-pillar,0]) cube([pillar,pillar,height]);
//Top of the box for reference
translate([0,0,height])
difference() {
cube([topWidth,topDepth,topThick]);
// Big red Button
translate([topWidth/2,topDepth/2,-1]) cylinder(h=topThick+2,d=BigRedButtonD);
// toggle switch
translate([topWidth/6,topDepth/2-6,-1]) cylinder(h=topThick+2,d=ToggleD);
// disarmed LED
translate([topWidth/6,topDepth/4,-1]) cylinder(h=topThick+2,d=WS2812D);
// Armed LED
translate([topWidth/6,topDepth/4-10,-1]) cylinder(h=topThick+2,d=WS2812D);
//text
translate([topWidth/2-10,topDepth-10,(topThick/2)+.5]) letter(Version);
//LED ring
translate([topWidth/2,topDepth/2,0])
for ( i = [0 : 360/numLEDs : 360] ){
rotate([0, 0, i]) translate([0, WS2812RingR, -1]) cylinder(h=topThick+2,d=WS2812D);
}
}
//add a right side with holes for rotary encoders or wait?
// wait?
// the side will have two rotary encoders
// one for Throttle and one for timewarp
// add Leds on the top ( + / toggle-Key / - )
// 3 LEDS for each encoder
module ringHolder() {
//module for led ring holder - DRAFT
holderH=2;
holderOutD=42;
holderInD=32;
holderFence=1;
holderFenceOutD=holderOutD+holderFence;
holderFenceInD=holderInD-holderFence;
difference() {
//holder ring
difference () {
cylinder(h=holderH+holderFence,d=holderFenceOutD);
translate([0,0,-1]) cylinder(h=holderH+holderFence+2,d=holderFenceInD);
}
//led ring for subtraction
translate([0,0,holderFence+1]) difference () {
cylinder(h=holderH,d=holderOutD);
translate([0,0,-1]) cylinder(h=holderH+2,d=holderInD);
}
}
}
//draft for now
//ringHolder();
The first test fitting worked well to show up some room for improvement:
-
Toggle switch moved left and down
-
legs longer
-
LED ring proper Radius